#!/usr/bin/env python3
"""Updates the wiki after a call, in its own short Claude Code session, so the call agent never stops listening
to write. Aux's call watcher (CallWatcher.swift) runs this each time the operator leaves a call:

  aux-after-call --env <prod|staging|dev> --call <call-id>

1. Waits until the operator has been off the call for a minute. Leaving is not always the end: they step into a
   sidebar (a separate call) and come back under the same id, or drop and rejoin. Back on it within the minute:
   nothing is written now, and this runs again the next time they leave.
2. Skips a call on which nothing was said, and one whose transcript has not grown since its last update.
3. Runs the wiki skill's "After a call" (`claude -p`): the call page, the people pages, home.md, one commit. It
   reads Tuple's stored transcript, so a ten-second call is recorded like any other, whether or not the agent
   heard it live. A call left and returned to is updated again in place.

One update per call at a time (a lock in the call's folder). The log is $AUX_HOME/after-call.log.
"""
import argparse
import fcntl
import os
import subprocess
import sys
import time
from pathlib import Path

# Inside an app bundle a __pycache__ write is macOS's "python3 was prevented from modifying apps" prompt.
sys.dont_write_bytecode = True
sys.path.insert(0, str(Path(__file__).resolve().parent))
import aux_context as ctx  # noqa: E402

AWAY = 60                  # seconds off the call before it counts as left
UNREACHABLE_FOR = 10 * 60  # seconds the Tuple client may stay unreachable before this gives up
MAX_TURNS = 60


def current(cli):
    """(reachable, the operator's current call id or "")."""
    code, out, err = ctx.run([cli, "call", "show", "--only", "id"])
    if code == 0 and len(out) == 36:
        return True, out
    text = (out + err).lower()
    return "not in a call" in text or "no active call" in text or code == 0, ""


def left(cli, call, away, poll, say):
    """True once the operator has been off the call for `away` seconds; False when they are back on it, or
    the client stayed unreachable (the update needs it for the transcript)."""
    off = unreachable = 0.0
    while True:
        reachable, cur = current(cli)
        if not reachable:
            unreachable += poll
            if unreachable > UNREACHABLE_FOR:
                say("the Tuple client stayed unreachable; no update")
                return False
        elif cur == call:
            say("the operator is back on the call; the update waits for them to leave")
            return False
        else:
            unreachable = 0.0
            off += poll
            if off >= away:
                return True
        time.sleep(poll)


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("--env", choices=ctx.CLIS, required=True)
    ap.add_argument("--call", required=True)
    ap.add_argument("--model", default="sonnet")
    ap.add_argument("--away", type=float, default=AWAY, help=argparse.SUPPRESS)       # tests
    ap.add_argument("--poll", type=float, default=5, help=argparse.SUPPRESS)
    ap.add_argument("--cli", help=argparse.SUPPRESS)
    a = ap.parse_args()
    try:
        os.setsid()   # outlive the app that started this
    except OSError:
        pass
    cli = a.cli or ctx.cli_path(a.env) or ctx.CLIS[a.env]
    folder = ctx.AUX / "calls" / a.call
    folder.mkdir(parents=True, exist_ok=True)
    log = open(ctx.AUX / "after-call.log", "a", buffering=1)

    def say(text):
        log.write(f"{time.strftime('%Y-%m-%d %H:%M:%S')} {a.call[:8]} {text}\n")

    lock = open(folder / "after-call.lock", "w")
    try:
        fcntl.flock(lock, fcntl.LOCK_EX | fcntl.LOCK_NB)
    except OSError:
        say("an update for this call is already waiting or running")
        return 0
    if not left(cli, a.call, a.away, a.poll, say):
        return 0

    spoken = sum(r.get("type") == "transcription_finished" for r in ctx.capture(cli, a.call))
    marker = folder / "after-call.json"
    if not spoken:
        say("nothing was said; no update")
        return 0
    if ctx.read_json(marker).get("spoken") == spoken:
        say("nothing was said since the last update")
        return 0

    ctx.status("wrapping_up", "Updating your wiki", a.call)
    prompt = (
        f"A Tuple call has just ended and this session writes it into the wiki: do the wiki skill's \"After a call\" "
        f"for call {a.call}. AUX_HOME is {ctx.AUX}. The Tuple CLI is `{cli}`: use it wherever the skill says $CLI. "
        "A very short call still gets a page, a few lines long. Nobody is watching this session, so ask no questions."
    )
    cmd = os.environ.get("AUX_AFTER_CALL_CMD", "").split() or [
        "claude", "-p", "--model", a.model, *ctx.claude_plugin_args(), "--permission-mode", "auto",
        "--max-turns", str(MAX_TURNS), "--append-system-prompt", prompt, f"{ctx.plugin_skill('wiki')} after-call {a.call}"]
    say(f"updating the wiki ({spoken} lines of speech)")
    started = time.time()
    try:
        code = subprocess.run(cmd, cwd=str(ctx.project()), stdout=log, stderr=subprocess.STDOUT, stdin=subprocess.DEVNULL,
                              env={**os.environ, "AUX_CALL": a.call}, timeout=30 * 60).returncode
    except (OSError, subprocess.TimeoutExpired) as e:
        say(f"the update did not run: {e}")
        code = 1
    say(f"update finished in {time.time() - started:.0f} s, exit {code}")
    if code == 0:
        ctx.write_json(marker, {"spoken": spoken, "at": int(time.time())})
    ctx.status("done", call=a.call)
    return code


if __name__ == "__main__":
    sys.exit(main())
